#!/usr/bin/python
# -*- coding:utf-8 -*-
"""
@file: clasificador.py

Este módulo incluye las implementaciones de los algoritmos KNN (K Nearest
Neighbors) y PCA (Principal Components Analysis), así como algunas funciones
auxiliares.

Clases:
=======

  - ResultadoACP

Funciones:
==========

  - eig
  - print_arr
  - print_mat
  - sort_arr
  - sort_mat
  - miKNN
  - acp

"""
try:
  import ipdb
except ImportError, e:
  print "No se encontró el módulo ipdb"

import time
import numpy
from matplotlib.pylab import hist

def muestra( msg, val = "" ):
    clk = "%-.02f" % time.clock()
    cad = chr(27) + "[1;32m" + "[{0:>7}]" + chr(27) + "[0m" + " {1:<64}{2:>15}"
    print cad.format( clk, msg, val)

class ResultadoACP:
    """No es mas que una estructura para devolver los resultados del ACP"""
    def __init__(self):
        self.mf              = None
        self.C               = None
        self.D               = None
        self.d               = None
        self.Vp              = None
        self.Yfg             = None
        self.Yft             = None
        self.ems             = None
        self.E               = None
        self.EOpt            = None
        self.clases_obt      = None
        self.clases_obt_test = None
        self.t_e             = list()
        self.t_e_test        = list()

    def aCadena( self ):

        string  = "\nPrueba.mf = "
        string += print_mat( self.mf )
        string += "\nPrueba.C = "
        string += print_mat( self.C )
        string += "\nPrueba.D = "
        string += print_mat( self.D )
        string += "\nPrueba.d = "
        string += print_arr( self.d )
        string += "\nPrueba.Vp = "
        string += print_mat( self.Vp )
        string += "\nPrueba.Yfg = "
        string += print_mat( self.Yfg )
        return string


def eig(mat):
    """
    Sustituto para la función eig de MATLAB

    Sea mat una matriz de M x M Se pretende sustituir la instrucción:

        [V D] = eig(M)

    La cual devuelve dos matrices de M x M. Las columnas de V son los vectores
    propios de mat. D es una matriz diagonal de M x M cuyos elementos no nulos
    corresponden a los valores propios de mat.

    @param mat: Matriz cuadrada
    @type mat: numpy.matrix

    @retval : Una tupla con tres elementos, los dos primeros corresponden a las
    matrices V, D respectrivamente, el tercer elemeto es un arreglo que contiene
    los valores propios de la matriz mat.
    """

    eigarr, V = numpy.linalg.eig(mat)
    (n,)      = eigarr.shape
    eigmat    = numpy.matrix( numpy.eye(n) )

    for i in range(n):
        eigmat[i,i] = eigarr[i]

    return V, eigmat, eigarr

def print_arr(arr):
    """
    Mueatra un arreglo

    Aplica un formato a los coeficientes del arreglo, para que sea más sencilla
    su vsualización

    @param arr: vector
    @type arr: numpy.array

    @retval: Cadena de caracteres que contiene la información del vector con el
    formato ya aplicado, esta cadena puede ser mostrada en pantalla o enviada
    directamente a un archivo
    """
    string = "[\n"
    s = "%-.04f" % arr[0]
    string += "{0:>10}".format(s)

    for num in arr[1:]:
        s = "%-.04f" % num
        string += ";\n{0:>10}".format(s)

    return string + "\n];\n"

def print_mat(mat):
    """
    Mueatra una matriz

    Aplica un formato a los coeficientes del arreglo, para que sea más sencilla
    su vsualización

    @param arr: vector
    @type arr: numpy.array

    @retval: Cadena de caracteres que contiene la información de la matriz con
    el formato ya aplicado, esta cadena puede ser mostrada en pantalla o enviada
    directamente a un archivo
    """
    string = "[\n"
    ren = mat[0]

    for item in ren.tolist():
        for num in item:
            snum = "%-.4f" % num # if num != 0 else "%d" % num
            string += " {0:>10}".format(snum)


    for ren in mat[1:]:

        string += ";\n"

        for item in ren.tolist():
            for num in item:
                snum = "%-.4f" % num # if num != 0 else "%d" % num
                string += " {0:>10}".format(snum)

    return string + "];\n"

def sort_arr(arr):
    """Sustituto para la función sort de MATLAB"""
    n = max(arr.shape)
    dic = dict( zip( arr.tolist(), range(n) ) )
    arr = numpy.sort(arr)
    idx = list()
    for num in arr:
        idx.append( dic[num] )

    return arr, numpy.array(idx)

def sort_mat(mat):
    """Sustituto para la función sort de MATLAB"""
    m, n = mat.shape

    m_ord = numpy.matrix( numpy.zeros( mat.shape ) )
    m_idx = numpy.matrix( numpy.zeros( mat.shape ) )

    M = numpy.array( mat.transpose() )

    i = 0
    for ren in M:

        col, idx    = sort_arr( numpy.array( ren ) )
        col         = ( numpy.matrix( col ) ).transpose()
        idx         = ( numpy.matrix( idx ) ).transpose()
        m_ord[:,i] += col
        m_idx[:,i] += idx
        i          += 1

    return m_ord, m_idx

def miKNN( train_patterns, train_targets, test_patterns, knn ):
    """Implementación del algoritmo K Nearest Neighbors"""

    L  = max(train_targets.shape)
    Uc = numpy.unique( train_targets + 1 )
    Uc = numpy.arange(41)

    if L < knn :
        raise ValueError("Se especificaron más vecinos que los existentes.")

    _, N       = test_patterns.shape
    test_targets = numpy.zeros( N )
    unos         = numpy.matrix( numpy.ones( (1,L) ) )

    for i in range(N):
        avance = "%.02f %s" % ( float(i) / float(N) * 100, r'%' )
        muestra( "Ejecutando clasificador", avance )

        dist       = sum (
            numpy.power(( train_patterns - test_patterns[:,i] * unos ), 2) )
        dist       = numpy.array(dist)
        _, b       = dist.shape
        dist       = dist.reshape((b,))
        _, indices = sort_arr(dist)
        del dist
        tran       = train_targets[ indices[:knn] ]

        n, _       = numpy.histogram( train_targets[ indices[:knn] ], Uc )

        best       = numpy.argmax( n )
        test_targets[i] = Uc[best]

    muestra( "Ejecutando clasificador", "[HECHO]" )

    return test_targets

def acp( vfaces, q = None ):
    """
    Implementacion del algoritmo de Analisis de componentes principales
    ACP, en ingles PCA
    """
    m,n = vfaces.shape

    P = ResultadoACP()         # Creando la estructura para devolver los resul-
                               # tados
    k, n = vfaces.shape        # Obtiene las dimensiones del vector vfaces

    if not q:                  # Si el número de argumentos es menor que dos
        q = n                  # entonces inicia q con el valor de n

    # Determinando la cara media
    P.mf = numpy.sum( vfaces, 1 ) / n  # Calculando la imágen media

    mf = numpy.repeat( P.mf, n ) # Es una "imitación" de la función original
    mf = mf.reshape((k,n))       # repmat de MATLAB

    A = vfaces - mf

    # Calculando la matriz de covarianza
    P.C = A.transpose() * A # Se tienen problemas para calcular este producto

    V, P.D, P.d = eig( P.C ) # Procuce una matriz diagonal P.D con los valores
                             # propios de la matriz de covarianza, una matriz
                             # cuadrada V cuyas columnas son los vectores
                             # propios correspondientes.
                             # P.d contiene los valores propios almacenados en
                             # un arreglo

    P.d, idx = sort_arr( P.d ) # Ordenando los valores propios de menor a mayor

    P.d = numpy.flipud( P.d )  # Invierte el orden de los valores propios, ahora
    idx = numpy.flipud( idx )  # están de mayor a menor.

    P.d = P.d / (n-1)
    P.D = numpy.matrix( numpy.diag( P.d ) )

    V = V[:,idx]

    V = A * V

    cpV = numpy.matrix(V)  # Una copia de V

    for i in range(n):
        V[:,i] = cpV[:,i] / numpy.linalg.norm( cpV[:,i] )

    P.Vp = V[:,:q+1]

    P.Yfg = P.Vp.transpose() * A
    del A
    del cpV
    del V
    return  P

